package bu.met.cs664;


import java.awt.*;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

/**
 * Created by mcox on 10/9/14.
 */
public class AgentPlayer extends Player {

    /** Set of valid move actions */
    private static final EnumSet<Actions> moves = EnumSet.of(Actions.MOVE_NORTH, Actions.MOVE_EAST, Actions.MOVE_SOUTH,
            Actions.MOVE_WEST);

    /** Map between a direction and the inverse direction */
    private static final Map<Actions, Actions> inverseMove = new HashMap<Actions, Actions>();

    /** Maps the directional orientation with the action to be performed */
    private static final Map<Actions, Direction> actionToDirection = new HashMap<Actions, Direction>();

    /** Maps direction to the corresponding move in that direction */
    private static final Map<Direction, Actions> directionToMove = new HashMap<Direction, Actions>();

    /** Maps direction to the corresponding shoot in that direction */
    private static final Map<Direction, Actions> directionToShoot = new HashMap<Direction, Actions>();

    static {
        inverseMove.put(Actions.MOVE_NORTH, Actions.MOVE_SOUTH);
        inverseMove.put(Actions.MOVE_EAST, Actions.MOVE_WEST);
        inverseMove.put(Actions.MOVE_SOUTH, Actions.MOVE_NORTH);
        inverseMove.put(Actions.MOVE_WEST, Actions.MOVE_EAST);

        actionToDirection.put(Actions.MOVE_NORTH, Direction.NORTH);
        actionToDirection.put(Actions.MOVE_EAST, Direction.EAST);
        actionToDirection.put(Actions.MOVE_SOUTH, Direction.SOUTH);
        actionToDirection.put(Actions.MOVE_WEST, Direction.WEST);
        actionToDirection.put(Actions.SHOOT_NORTH, Direction.NORTH);
        actionToDirection.put(Actions.SHOOT_EAST, Direction.EAST);
        actionToDirection.put(Actions.SHOOT_SOUTH, Direction.SOUTH);
        actionToDirection.put(Actions.SHOOT_WEST, Direction.WEST);

        directionToMove.put(Direction.NORTH, Actions.MOVE_NORTH);
        directionToMove.put(Direction.EAST, Actions.MOVE_EAST);
        directionToMove.put(Direction.SOUTH, Actions.MOVE_SOUTH);
        directionToMove.put(Direction.WEST, Actions.MOVE_WEST);

        directionToShoot.put(Direction.NORTH, Actions.SHOOT_NORTH);
        directionToShoot.put(Direction.EAST, Actions.SHOOT_EAST);
        directionToShoot.put(Direction.SOUTH, Actions.SHOOT_SOUTH);
        directionToShoot.put(Direction.WEST, Actions.SHOOT_WEST);
    }

    /** Knowledge base of what the agent currently knows of the game board */
    private final MoveEvaluator moveEvaluator;

    /** History of all moves taken from the origin */
    private final Stack<Actions> moveHistory = new Stack<Actions>();

    /** Current facing direction */
    private Direction direction;

    /** Exit square */
    private Point exit;

    /** Current x,y coordinate */
    private Point currentPosition;

    /** Current size of the board */
    private int boardSize;

    /** Does the agent have the gold */
    private boolean hasGold;

    /** Does the agent have an arrow */
    private boolean hasArrow = true;

    /** Initialize with a start position */
    public AgentPlayer(int boardSize, int [] position, Direction direction) {
        super(position);
        updatePosition(getPosition());
        this.boardSize = boardSize;
        this.exit = new Point(currentPosition.x, currentPosition.y);
        this.moveEvaluator = new MoveEvaluator(boardSize);
        this.direction = direction;
    }

    /** Determines the agents next move after updating the knowledge base with the current sensory input. */
    @Override
    public Actions getAction(String inputs) {
        EnumSet<Attributes> attributes = Attributes.fromString(inputs);
        updatePosition(getPosition());
        moveEvaluator.updateSquare(this.currentPosition.x, this.currentPosition.y, inputs);

        if (attributes.contains(Attributes.SCREAM)) {
            moveEvaluator.deadWumpus();
        }

        return generateNextAction(attributes);
    }

    /**
     * Determines the agents next action in conjunction with the knowledge base. The agent follows the following
     * opportunistic, but safe decision hierarchy.
     *
     * Standing on, or holding gold
     * 1. If standing on the gold pick it up.
     * 2. If holding gold back track to the origin and escape
     *
     * Search
     * 1. Find the first unvisited safe square, turn towards it and move in that direction
     * 2. If next to wumpus shoot it
     * 3. If no safe square backtrack and repeat 1.
     *
     * Bail
     * 1. If on origin with no unvisited safe squares exit
     *
     * @param attributes
     * @return
     */
    private Actions generateNextAction(EnumSet<Attributes> attributes) {

        //
        // Standing on, or holding gold
        //

        if (this.hasGold) {
            if (this.currentPosition.equals(exit)) {
                return Actions.EXIT;
            }

            return backtrack();
        }

        if (attributes.contains(Attributes.GOLD)) {
            this.hasGold = true;
            return Actions.PICK_UP_GOLD;
        }

        //
        // Searching
        //

        for (Direction direction : Direction.values()) {
            Point p = adjacentSquare(direction);

            if (p != null && !moveEvaluator.isVisited(p.x, p.y) && moveEvaluator.isSafe(p.x, p.y)) {
                turnTowards(direction);
                recordMove(directionToMove.get(direction));
                return directionToMove.get(direction);
            }
        }

        if (hasArrow) {
            for (Direction direction : Direction.values()) {
                Point p = adjacentSquare(direction);

                if (p != null && moveEvaluator.definitelyAWumpus(p.x, p.y)) {
                    this.hasArrow = false;
                    turnTowards(direction);
                    return directionToShoot.get(direction);
                }
            }
        }

        Actions backup = backtrack();
        if (backup != null) {
            return backup;
        }

        //
        // Bail, searching above should have exhausted all obtainable movements and returned us to the origin
        //

        return Actions.EXIT;
    }

    /** Return the currentPosition of the square in the provided direction */
    private Point adjacentSquare(Direction d) {
        Point adjacentSquare = null;

        switch (d) {
            case NORTH:
                adjacentSquare = new Point(this.currentPosition.x, this.currentPosition.y - 1);
                break;
            case EAST:
                adjacentSquare = new Point(this.currentPosition.x + 1, this.currentPosition.y);
                break;
            case SOUTH:
                adjacentSquare = new Point(this.currentPosition.x, this.currentPosition.y + 1);
                break;
            case WEST:
                adjacentSquare = new Point(this.currentPosition.x - 1, this.currentPosition.y);
                break;
        }

        if (adjacentSquare.x < 0 || adjacentSquare.y < 0
                || adjacentSquare.x > boardSize -1 || adjacentSquare.y > boardSize -1) {
            return null;
        }

        return adjacentSquare;
    }

    /** Add a move to the move history */
    private void recordMove(Actions a) {
        if (moves.contains(a)) {
            moveHistory.push(a);
        } else {
            throw new IllegalArgumentException(String.format("Unable to record %s as a move", a));
        }

    }

    /** Translate an array of x,y coordinates to internal representation */
    private void updatePosition(int [] position) {
        this.currentPosition = new Point(position[0], position[1]);
    }

    /** Turn agent to the right */
    private void turnTowards(Direction d) {
        while (this.direction != d) {
            this.direction = Direction.turnRight(this.direction);
        }
    }

    /** If we are not at the origin, turn and move to the square we just came from */
    private Actions backtrack() {
        if (moveHistory.isEmpty()) {
            return null;
        }

        Actions nextMove = inverseMove.get(moveHistory.peek());
        turnTowards(actionToDirection.get(nextMove));
        return inverseMove.get(moveHistory.pop());
    }
}